//*********************************************************
//
//    Copyright (c) Microsoft. All rights reserved.
//    This code is licensed under the Apache License, Version 2.0.
//    THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
//    ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
//    IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
//    PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
//
//*********************************************************

namespace Microsoft.Research.eResearch.Execution
{
    using System;
    using System.Globalization;
    using System.IO;
    using System.Threading;
    using System.Workflow.Runtime;
    using Microsoft.Research.DataLayer;
    using Microsoft.Research.eResearch.Common;
    using Microsoft.Research.eResearch.Common.Validation;
    using TridentContext;
    using Microsoft.Research.ScientificWorkflow.TridentUtilities;
    using System.Text;
    using System.Reflection;

    /// <summary>
    /// Class for running the WF using the job object.
    /// </summary>
    internal class JobExecutor : IDisposable
    {
        /// <summary>
        /// Instance of WorkflowExecutor.
        /// </summary>
        private WorkflowExecutor workflowExecutor;

        /// <summary>
        /// Instance of JobMonitor.
        /// </summary>
        private JobMonitor jobMonitor;

        /// <summary>
        /// Instance of the WorkflowActivityModel generator.
        /// </summary>
        private WorkflowActivityModel model;

        /// <summary>
        /// Internal job status used to raise the event only once whenever status is changed by the user.
        /// </summary>
        private InternalJobStatus jobStatus = InternalJobStatus.Running;

        /// <summary>
        /// Flag to indicate whether this object is disposed already.
        /// </summary>
        private bool disposed;

        /// <summary>
        /// Registry connection.
        /// </summary>
        private Connection registryConnection;

        ///  Wait Handle.
        /// </summary>
        private AutoResetEvent waitHandle = new AutoResetEvent(false);

        /// <summary>
        /// Result of the job
        /// </summary>
        private int jobResult = -1;

        /// <summary>
        /// Public property to expose the job result
        /// </summary>
        public int JobResult
        {
            set { jobResult = value; }
            get { return jobResult; }
        }

        /// <summary>
        /// This method opens a new connection to the registry and reads the
        /// job details after loading the job from there.
        /// It then sets up all the data for running the WF and then loads 
        /// and runs it.
        /// </summary>
        /// <param name="jobId">Currently running job Id.</param>
        /// <param name="targetFolder">Target folder path.</param>
        /// <param name="libFolder">Lib folder path.</param>
        public void Execute(Guid jobId, string targetFolder, string libFolder, bool isLightWeightExecution)
        {
            ThrowIfDisposed();

            // Validate arguments
            CustomValidator.AssertNotNull(jobId, "jobId");
            CustomValidator.AssertNotEmpty(targetFolder, "targetFolder");
            CustomValidator.AssertRule<string>(path => !Directory.Exists(path), targetFolder, string.Format(CultureInfo.CurrentCulture, "{0} does not exists", targetFolder));
            CustomValidator.AssertRule<string>(path => !Directory.Exists(path), libFolder, string.Format(CultureInfo.CurrentCulture, "{0} does not exists", libFolder));
            ExceptionHandler.Handle(
            PolicyName.LoggingPolicy,
            () =>
            {
                // Create a new instance of JobMonitor.
                jobMonitor = new JobMonitor(jobId, isLightWeightExecution);
                jobMonitor.StopRequested += new EventHandler<JobStatusEventArgs>(JobStopRequested);
                jobMonitor.PauseRequested += new EventHandler<JobStatusEventArgs>(JobPauseRequested);

                ExecuteIfYouCan(() =>
                {
                    // Create registry connection.
                    CreateRegistryConnection(isLightWeightExecution);

                    // Set the context object for the activities to use.
                    CreateContext(jobId);
                });

                // Start the monitor thread. 
                // Note: Start code is placed here instead of on top of the above block to avoid 
                // concurrency issue
                jobMonitor.Start();

                ExecuteIfYouCan(() =>
                {
                    // Create the sanbox environment and download the dlls.
                    Sandbox.Create(jobId, targetFolder, libFolder, isLightWeightExecution);
                });

                ExecuteIfYouCan(() =>
                {
                    try
                    {
                        // Generate the System.ComponentModel.Activity from the registry representation.
                        // Also sets the workflow parameters
                        model = WorkflowActivityModel.Generate(jobId, libFolder, isLightWeightExecution);
                    }
                    catch (Exception exp)
                    {
                        jobMonitor.Terminated(exp.Message);
                        throw;
                    }
                });

                ExecuteIfYouCan(() =>
                {
                    try
                    {
                        // Create a new instance of WorkflowExecutor.
                        workflowExecutor = WorkflowExecutor.CreateWorkflowExecutor(jobId, model.LoadedAssemblies, jobMonitor.WorkflowName, model.XomlPath, registryConnection, isLightWeightExecution);
                        workflowExecutor.WorkflowCompleted += new EventHandler<WorkflowCompletedEventArgs>(WorkflowExecutionCompleted);
                        workflowExecutor.WorkflowTerminated += new EventHandler<WorkflowTerminatedEventArgs>(WorkflowExecutionTerminated);
                    }
                    catch (System.Workflow.ComponentModel.Compiler.WorkflowValidationFailedException validationException)
                    {
                        StringBuilder errors = new StringBuilder();

                        foreach (System.Workflow.ComponentModel.Compiler.ValidationError validationError in validationException.Errors)
                        {
                            ExceptionHandler.Handle(PolicyName.LoggingPolicy, new Exception(validationError.ToString()));
                            errors.AppendLine(validationError.ErrorText);
                        }

                        jobMonitor.Terminated(errors.ToString());
                        throw;
                    }
                    catch (Exception exp)
                    {
                        jobMonitor.Terminated(exp.Message);
                        throw;
                    }
                });

                ExecuteIfYouCan(() =>
                {
                    // Execute the workflow.
                    workflowExecutor.Execute();

                    // Wait for workflow to complete.
                    waitHandle.WaitOne();
                });

                // Wait for the monitor thread to terminate.
                jobMonitor.JoinThread();
            },
            () =>
            {
                // Close the registry connection.
                CloseRegistryConnection();
            });
        }

        /// <summary>
        /// Dispose this object and releases all the managed resources it contains.
        /// </summary>
        public void Dispose()
        {
            ThrowIfDisposed();

            waitHandle.Close();
            waitHandle = null;

            if (workflowExecutor != null)
            {
                workflowExecutor.Dispose();
                workflowExecutor = null;
            }

            GC.SuppressFinalize(this);
            this.disposed = true;
        }

        /// <summary>
        /// Execute the actions if user is not stop or pause requested.
        /// </summary>
        /// <param name="action">Instance of the action object.</param>
        private void ExecuteIfYouCan(System.Action action)
        {
            if (jobStatus == InternalJobStatus.StopRequested)
            {
                jobStatus = InternalJobStatus.None;
                jobMonitor.Terminated(null);
            }
            else if (jobStatus == InternalJobStatus.PauseRequested)
            {
                jobStatus = InternalJobStatus.None;
                jobMonitor.Paused();
            }
            else if (jobStatus == InternalJobStatus.Running)
            {
                action();
            }
        }

        /// <summary>
        /// Raised whenever stop request for the job is raised by the user.
        /// </summary>
        /// <param name="sender">Instance of the job monitor.</param>
        /// <param name="e">Empty event argument.</param>
        private void JobStopRequested(object sender, JobStatusEventArgs e)
        {
            jobStatus = InternalJobStatus.StopRequested;
            if (workflowExecutor != null && workflowExecutor.IsStarted)
            {
                workflowExecutor.Terminate(e.Message);
            }
        }

        /// <summary>
        /// Raised whenever pause request for the job is raised by the user.
        /// </summary>
        /// <param name="sender">Instance of the job monitor.</param>
        /// <param name="e">Empty event argument.</param>
        private void JobPauseRequested(object sender, EventArgs e)
        {
            jobStatus = InternalJobStatus.PauseRequested;
            if (workflowExecutor != null && workflowExecutor.IsStarted)
            {
                workflowExecutor.Pause();
            }
        }

        /// <summary>
        /// Open the Registry from config file whihc has Default Connection Set.
        /// </summary>
        private void CreateRegistryConnection(bool isLightWeightExecution)
        {
            SR_Connection.Init();
            ConnectionManager connectionManager = null;

            if (isLightWeightExecution)
            {
                Assembly me = Assembly.GetExecutingAssembly();
                string myPath = Path.GetDirectoryName(me.Location);

                //Put the alternate location
                ConnectionManager.AlternateProviderLocation = myPath;

                connectionManager = ConnectionManager.CreateCustom(
                    ConnectionManager.CancelBehavior.ThrowException,
                    Path.Combine(myPath, "ConnStore-Application.configSources"));
            }
            else
            {
                connectionManager = ConnectionManager.CreateForAgent(ConnectionManager.CancelBehavior.ThrowException, ConnectionManager.Agent.ExecutionService);
            }

            registryConnection = connectionManager.PickConnection(ConnectionManager.ConnectionUI.NeverShowUI);
            registryConnection.AutoSave = false;

            TridentAuthentication authenticate = new TridentAuthentication(this.registryConnection);
            authenticate.AuthenticateUser();
        }

        /// <summary>
        /// This method initializes the trident run time. It creates the
        /// context object for the running WF instance. The context object
        /// provides run time services like logging, status reporting etc.
        /// </summary>    
        /// <param name="jobID">Job Id for data product.</param>        
        private void CreateContext(Guid jobID)
        {
            Context.Create(this.registryConnection, this.jobMonitor.ActivityInstance, jobID);
            Context.BuildActivitySequence(this.jobMonitor.ActivityInstance.Activity.BaseSequence);
        }

        /// <summary>
        /// Raised whenever workflow runtime terminates the workflow instance.
        /// </summary>
        /// <param name="sender">Current workflow runtime instance.</param>
        /// <param name="e">Terminate event arguements.</param>
        private void WorkflowExecutionTerminated(object sender, WorkflowTerminatedEventArgs e)
        {
            ExceptionHandler.Handle(PolicyName.LoggingPolicy, e.Exception);
            JobResult = -2;
            jobMonitor.Terminated(e.Exception.Message);
            waitHandle.Set();
        }

        /// <summary>
        /// Raised whenever workflow runtime completes the workflow instance.
        /// </summary>
        /// <param name="sender">Current workflow runtime instance.</param>
        /// <param name="e">Completed event arguements.</param>
        private void WorkflowExecutionCompleted(object sender, WorkflowCompletedEventArgs e)
        {
            Console.WriteLine("Job Completed successfully");
            JobResult = 0;
            jobMonitor.Completed();
            waitHandle.Set();
        }

        /// <summary>
        /// Deletes all the temporary Files Created.
        /// </summary>
        private void CloseRegistryConnection()
        {
            ExceptionHandler.Handle(
            "Logging Policy",
            () =>
            {
                // Close the registry Connection.
                if (registryConnection != null)
                {
                    registryConnection.Close();
                    registryConnection = null;
                }
            });
        }

        /// <summary>
        /// Throws the ObjectDisposedException if operation is performed on the disposed object. 
        /// </summary>
        private void ThrowIfDisposed()
        {
            if (disposed)
            {
                throw new ObjectDisposedException("JobExecutor");
            }
        }
    }
}
